package com.dbw.pdftool;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.Scanner;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.interactive.documentnavigation.destination.PDPageXYZDestination;
import org.apache.pdfbox.pdmodel.interactive.documentnavigation.outline.PDDocumentOutline;
import org.apache.pdfbox.pdmodel.interactive.documentnavigation.outline.PDOutlineItem;

import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.stream.JsonReader;

/**
 * 书签生成器。
 * 
 * @author dongb
 *
 */
public class SimpleBookmarkMaker {

    /**
     * 每一级的书签描述。
     * 
     * <p>
     * 用于匹配及解析书签。
     * 
     * @author dongb
     *
     */
    static class BookmarkItemDescription {

        /**
         * 描述书签的正则表达式，其中必须包含书签名和页码的分组。
         * <p>
         * 比如：<tt>^([^~]+)~+\\s*(\\d+)</tt>，其中第一个括号对应着书签名， 第二个括号对应着页码。
         */
        private String pattern;

        /**
         * 由{@link #pattern}编译的正则实例
         */
        private transient Pattern compiledPattern = null;

        /**
         * 书签名在{@link #pattern pattern}中的分组号
         */
        private int nameIndex;

        /**
         * 页码在{@link #pattern pattern}中的分组号
         */
        private int pageIndex;

        /**
         * 构造一个书签描述项
         * 
         * @param pattern
         *            描述书签的正则表达式
         * @param nameIndex
         *            书签名在{@link #pattern pattern}中的分组号
         * @param pageIndex
         *            页码在{@link #pattern pattern}中的分组号
         */
        public BookmarkItemDescription(String pattern, int nameIndex, int pageIndex) {
            this.pattern = pattern;
            this.nameIndex = nameIndex;
            this.pageIndex = pageIndex;
        }

        public String getPattern() {
            return pattern;
        }

        public void setPattern(String pattern) {
            this.pattern = pattern;
        }

        public int getNameIndex() {
            return nameIndex;
        }

        public void setNameIndex(int nameIndex) {
            this.nameIndex = nameIndex;
        }

        public int getPageIndex() {
            return pageIndex;
        }

        public void setPageIndex(int pageIndex) {
            this.pageIndex = pageIndex;
        }

        public Pattern getCompiledPattern() {
            synchronized (this) {
                if (compiledPattern == null) {
                    if (pattern == null) {
                        throw new IllegalStateException("pattern is empty");
                    }
                    compiledPattern = Pattern.compile(pattern);
                }

                return compiledPattern;
            }
        }
    }

    /**
     * 书签整体格式的描述，包含每级书签的描述列表以及其他通用信息。
     * 
     * @author dongb
     *
     */
    static class BookmarkDescription {

        /**
         * 每级书签描述列表，从先到后按级次排列。
         */
        private ArrayList<BookmarkItemDescription> itemDescList = new ArrayList<BookmarkItemDescription>();

        /**
         * 页码的偏移量。
         * 
         * <p>
         * 书签文件中描述的页数，可能并不是该页在PDF中的实际页数，而需要加上pageOffset这个依稀量。
         */
        private int pageOffset = 0;

        public ArrayList<BookmarkItemDescription> getItemDescList() {
            return itemDescList;
        }

        public void setItemDescList(ArrayList<BookmarkItemDescription> itemDescList) {
            this.itemDescList = itemDescList;
        }

        public void addItemDesc(BookmarkItemDescription item) {
            itemDescList.add(item);
        }

        public int getPageOffset() {
            return pageOffset;
        }

        public void setPageOffset(int pageOffset) {
            this.pageOffset = pageOffset;
        }

    }

    /**
     * 书签描述
     */
    private BookmarkDescription bookmarkDescription = null;

    /**
     * 书签根节点
     */
    private PDOutlineItem bookmarkRoot = null;

    /**
     * 父书签栈
     */
    private LinkedList<PDOutlineItem> parentBookmarkStack = null;

    /**
     * 最后一次添加的书签
     * <p>
     * 若之后添加的是下一级书签，则需要将其添加到<tt>lastBookmark</tt>
     */
    private PDOutlineItem lastBookmark = null;

    /**
     * 加载书签描述
     * 
     * @param inputPatternFile
     *            文本格式的书签描述文件路径
     * 
     * @throws Exception
     *             解析inputPatternFile失败时，将抛出此异常
     */
    private void loadBookmarkDescription(String inputPatternFile) throws Exception {

        if (inputPatternFile == null) {
            bookmarkDescription = createStandardBookmarkDescription();
            return;
        }

        Gson gson = new Gson();
        JsonReader jsonReader = null;

        jsonReader = new JsonReader(new InputStreamReader(new FileInputStream(inputPatternFile), "UTF-8"));
        bookmarkDescription = gson.fromJson(jsonReader, BookmarkDescription.class);
    }

    /**
     * 加载书签
     * 
     * @param inputBookmarkFile
     *            文本格式的书签文件路径
     */
    private void loadBookmark(String inputBookmarkFile) throws Exception {
        Scanner scanner = null;

        bookmarkRoot = new PDOutlineItem();
        bookmarkRoot.setTitle("Root");
        parentBookmarkStack = new LinkedList<PDOutlineItem>();
        parentBookmarkStack.push(bookmarkRoot);
        lastBookmark = null;

        try {
            scanner = new Scanner(new File(inputBookmarkFile));
            while (scanner.hasNextLine()) {
                String line = scanner.nextLine();

                int level;
                BookmarkItemDescription bookmarkItemDesc = null;
                Matcher bookmarkMatcher = null;

                // 使用各级书签描述去匹配
                for (level = bookmarkDescription.getItemDescList().size() - 1; level >= 0 ; level--) {
                    BookmarkItemDescription itemDesc = bookmarkDescription.getItemDescList().get(level);
                    Matcher matcher = itemDesc.getCompiledPattern().matcher(line);
                    if (matcher.find()) {
                        bookmarkItemDesc = itemDesc;
                        bookmarkMatcher = matcher;
                        break;
                    }
                }
                if (bookmarkItemDesc == null || bookmarkMatcher == null) {
                    continue;
                }

                // System.out.printf("[%s] match level %d\r\n", line, level +
                // 1);
                // 创建书签
                PDOutlineItem bookmark = createBookmarkItem(bookmarkMatcher.group(bookmarkItemDesc.nameIndex),
                        bookmarkMatcher.group(bookmarkItemDesc.pageIndex));
                if (bookmark == null) {
                    System.err.println("Can't create bookmark for: " + line);
                    continue;
                }

                // 将书签添加到书签集中
                addBookmarkItem(bookmark, level + 1);
            }
        } finally {
            if (scanner != null) {
                scanner.close();
            }
        }
    }

    private PDOutlineItem createBookmarkItem(String title, String pageNum) {

        PDOutlineItem bookmarkItem = new PDOutlineItem();
        int pageIndex = 0;

        // 设置标题
        bookmarkItem.setTitle(title);

        // 设置页数
        PDPageXYZDestination dest = new PDPageXYZDestination();
        try {
            pageIndex = Integer.parseInt(pageNum);
        } catch (NumberFormatException e) {
            return null;
        }
        pageIndex += bookmarkDescription.getPageOffset();
        dest.setPageNumber(pageIndex);
        bookmarkItem.setDestination(dest);

        return bookmarkItem;
    }

    private void addBookmarkItem(PDOutlineItem newItem, int level) throws IOException {

        if (level == parentBookmarkStack.size()) {
            // 级数不变
            parentBookmarkStack.peek().addLast(newItem);
        } else if (level > parentBookmarkStack.size()) {
            // 级数增加
            /* 将上次的书签添加到父书签栈 */
            if (lastBookmark != null) {
                parentBookmarkStack.push(lastBookmark);
            }
            /*
             * 若书签级别只是加1，此时parentBookmarkStack的大小应该与level相等。
             * 若还是不等，说明发生了跳级。先自动补充缺少的书签。
             */
            while (level > parentBookmarkStack.size()) {
                PDOutlineItem blankItem = new PDOutlineItem();
                blankItem.setDestination(newItem.getDestination());
                blankItem.setTitle("blank");
                parentBookmarkStack.peek().addLast(blankItem);
            }
            parentBookmarkStack.peek().addLast(newItem);
        } else {
            // 级数减小
            while (level < parentBookmarkStack.size()) {
                parentBookmarkStack.pop();
            }
            parentBookmarkStack.peek().addLast(newItem);
        }

        lastBookmark = newItem;
    }

    /**
     * 自动生成书签
     * 
     * @param inputPDF
     *            待生成书签的PDF文件路径
     * @param outputPDF
     *            添加书签后的PDF文件路径
     * 
     * @throws Exception
     */
    private void createBookmark(String inputPDF, String outputPDF) throws Exception {
        PDDocument document = null;
        try {
            document = PDDocument.load(new File(inputPDF));
            if (document.isEncrypted()) {
                throw new Exception("Cannot add bookmarks to encrypted document.");
            }

            PDDocumentOutline outline = new PDDocumentOutline();
            document.getDocumentCatalog().setDocumentOutline(outline);
            outline.addLast(bookmarkRoot);
            bookmarkRoot.openNode();
            outline.openNode();

            document.save(outputPDF);
        } finally {
            if (document != null) {
                document.close();
            }
        }
    }

    /**
     * 自动生成书签
     * 
     * @param inputPDF
     *            待生成书签的PDF文件路径
     * @param inputBookmarkFile
     *            文本格式的书签文件路径
     * @param inputPatternFile
     *            文本格式的书签描述文件路径
     * @param outputPDF
     *            添加书签后的PDF文件路径
     */
    public static void makeBookmark(String inputPDF, String inputBookmarkFile, String inputPatternFile,
            String outputPDF) {

        SimpleBookmarkMaker maker = new SimpleBookmarkMaker();

        try {
            maker.loadBookmarkDescription(inputPatternFile);
            maker.loadBookmark(inputBookmarkFile);
            maker.createBookmark(inputPDF, outputPDF);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * 创建标准的书签描述
     * 
     * @return
     */
    private static BookmarkDescription createStandardBookmarkDescription() {
        BookmarkDescription desc = new BookmarkDescription();
        desc.addItemDesc(new BookmarkItemDescription("^([^\t][^~]+)~+\\s*(\\d+)", 1, 2));
        desc.addItemDesc(new BookmarkItemDescription("^\t([^\t][^~]+)~+\\s*(\\d+)", 1, 2));
        desc.addItemDesc(new BookmarkItemDescription("^\t\t([^\t][^~]+)~+\\s*(\\d+)", 1, 2));
        desc.setPageOffset(0);

        return desc;
    }

    /**
     * 生成标准的书签描述，以供修改
     * 
     */
    public static void makeStandardBookmarkDescription(String outputFile) {
        BookmarkDescription desc = createStandardBookmarkDescription();
        Writer writer = null;

        try {
            FileOutputStream outputStream = new FileOutputStream(outputFile != null ? outputFile : "std_pattern.json");
            writer = new OutputStreamWriter(outputStream, "UTF-8");
            Gson gson = new GsonBuilder().serializeNulls().setPrettyPrinting().create();
            gson.toJson(desc, writer);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if (writer != null) {
                try {
                    writer.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}
